﻿// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. 
// See License.txt in the project root for license information.
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Actors;
using System.Threading.Tasks;

namespace TestActorMethods
{
    public class PartitionedActor
    {
        // Models problems that can arise when partitioned actors share the same IActorState.
        // We can guarantee this happens by having an actor call methods on itself via a proxy.
        [ActorMethod]
        public static Object CallSameActorMethodSynchronously(IActorState state, Object[] parameters)
        {
            String actorName = (String)parameters[0];
            IActorProxy actorProxy = state.GetActorProxy(actorName, null);  // Not partitioned
            IActorPromise promise = actorProxy.Request("MethodCalledSynchronously", new Object[] { "Hi" });
            // Waiting here may cause a deadlock.  If the actor is located in the same partition as this one,
            // then this method and the target method being called will both be trying to take the same lock.
            promise.Wait();
            String ret = promise.GetTypedResult<String>();
            if (!String.Equals(ret, "Hi"))
                throw new Exception(String.Format("We called MethodCalledSynchronously passing it one string, but didn't get that string back as its return value!  Got: {0}", ret));

            return "CallSameActorMethodSynchronously succeeded!";
        }

        [ActorMethod]
        public static Object MethodCalledSynchronously(IActorState state, Object[] parameters)
        {
            return parameters[0];
        }

        [ActorMethod]
        public static Object CreatePartitionedActorViaActorMethod(IActorState state, Object[] parameters)
        {
            int numPartitions = 5;
            state.CreateActor("TestActor", "applicationType=fabric:/actor/adhoc;serviceType=EmptyActorReplica", numPartitions);

            // Write different data to each partition
            Assembly assembly = typeof(SimpleFieldAccessActor).Assembly;
            String assemblyName = assembly.GetName().Name;
            byte[] assemblyBytes = File.ReadAllBytes(assembly.Location);
            String firstKey = "First key";
            String secondKey = "Second key";
            IActorProxy actorProxy1 = state.GetActorProxy("TestActor", firstKey);
            actorProxy1.AddAssembly(assemblyName, assemblyBytes);
            IActorProxy actorProxy2 = state.GetActorProxy("TestActor", secondKey);
            actorProxy2.AddAssembly(assemblyName, assemblyBytes);

            // Write different data to each partition
            actorProxy1.Command("SetX", new Object[] { firstKey, 1 });
            actorProxy2.Command("SetX", new Object[] { secondKey, 2 });

            // Now verify both values (presumably on different partitions) still contain the right value.
            IActorPromise promise = actorProxy1.Request("GetX", new Object[] { firstKey });
            int x = promise.GetTypedResult<int>();
            if (x != 1)
                throw new Exception(String.Format("Actor proxy 1 (to the first partition) should have returned 1.  Got: {0}", x));

            promise = actorProxy2.Request("GetX", new Object[] { secondKey });
            x = promise.GetTypedResult<int>();
            if (x != 2)
                throw new Exception(String.Format("Actor proxy 2 (to the first partition) should have returned 2.  Got: {0}", x));

            // Now try the same experiment on a lot of actor instances on the same partitioned service instance
            int numActors = numPartitions * 6;
            for (int i = 0; i < numActors; i++)
            {
                IActorProxy actorProxy = state.GetActorProxy("TestActor", "key "+i);
                actorProxy.AddAssembly(assemblyName, assemblyBytes);
                IActorPromise setPromise = actorProxy.Request("SetX", new Object[] { "key "+i, i });
                setPromise.Wait();
            }
            GC.Collect();
            for (int i = 0; i < numActors; i++)
            {
                IActorProxy actorProxy = state.GetActorProxy("TestActor", "key " + i);
                IActorPromise getPromise = actorProxy.Request("GetX", new Object[] { "key " + i });
                x = getPromise.GetTypedResult<int>();
                if (x != i)
                    throw new Exception(String.Format("When setting then getting values from a large number of partitioned actors, expected actor \"key {0}\"'s value to be {0}, but we got {1}", i, x));
            }

            // Now try calling a method that throws an exception
            IActorPromise throwsPromise = actorProxy1.Request("ThrowDivideByZeroException", new Object[0]);
            try
            {
                throwsPromise.Wait();
                throw new Exception("Expected IActorPromise.Wait to throw when waiting on a method that fails!");
            }
            catch (AggregateException ae)
            {
                if (ae.InnerExceptions.Count != 1)
                    throw;
                if (ae.InnerException.GetType() != typeof(DivideByZeroException))
                    throw;
                // Good - we saw an AggregateException wrapping one DivideByZeroException
            }

            if (!throwsPromise.IsFaulted)
                throw new Exception("Expected calling an actor method that threw an exception to be marked as faulted after waiting for it to complete.");
            if (throwsPromise.ExceptionType != "DivideByZeroException")
                throw new Exception(String.Format("When calling an actor method that throws a DivideByZeroException, I expected to get back a DivideByZeroException!  Got: {0}  Message: {1}", throwsPromise.ExceptionType, throwsPromise.ExceptionMessage));

            return "succeeded";
        }
    }

    public class SimpleFieldAccessActor
    {
        [ActorMethod]
        public static Object GetX(IActorState state, Object[] parameters)
        {
            String actorInstanceName = (String)parameters[0];
            return state.Get(actorInstanceName + "$X");
        }

        [ActorMethod]
        public static Object SetX(IActorState state, Object[] parameters)
        {
            String actorInstanceName = (String)parameters[0];
            state.Set(actorInstanceName + "$X", parameters[1]);
            return null;
        }

        [ActorMethod]
        public static Object ThrowDivideByZeroException(IActorState state, Object[] parameters)
        {
            throw new DivideByZeroException("Simulating that you divided by zero.");
        }
    }


    [TestClass]
    public class PartitionedActorTests
    {
        /*
         * This test hangs currently because we take a lock when evaluating an actor method, 
         * then calling another method via a proxy requires acquiring the same lock on IActorState.
         * The intent is to model the communication problems we can run into when partitioned actors
         * are co-located on the same IActorState, but we can deterministically reproduce this with 
         * one actor calling methods on itself too.
        [TestMethod]
        public void A2ASyncMethodCallTest()
        {
            Console.WriteLine("Testing Actor to Actor synchronous method calls, with blocking in the actor method.");
            IActorState state = ActorUnitTests.CreateActorState(ActorStateEnum.Standalone);
            String actorName = "fabric:/actor/adhoc/PartitionedActor";
            IActor actor = new StandaloneActor(state, actorName,
                new String[] { "CallSameActorMethodSynchronously", "MethodCalledSynchronously" },
                new Func<IActorState, Object[], Object>[] { PartitionedActor.CallSameActorMethodSynchronously, 
                                                            PartitionedActor.MethodCalledSynchronously });

            String json = actor.CallMethod("FakeClient", 1, "CallSameActorMethodSynchronously", 
                                           new String[] { SerializationHelper.Serialize(actorName) });
            ActorServiceResult result = ActorServiceResult.FromJson(json);

            if (result.Status != ActorServiceResultStatus.Succeeded)
                throw new Exception(String.Format("Calling an actor method failed!  {0}: {1}\r\n{2}", result.ExceptionType, 
                                                  result.ExceptionMessage, result.ExceptionStackTrace));
            if (!result.Result.Contains("succeeded"))
                throw new Exception(String.Format("Actor to Actor Synchronous method call test failed!  Return value: {0}", result));
        }
         */

        [TestMethod]
        public void CreatePartitionedActorViaActorMethodTest()
        {
            Console.WriteLine("Testing creating a partitioned actor from within an actor method.");
            IActorState state = ActorUnitTests.CreateActorState(ActorStateEnum.Standalone);
            String actorName = "fabric:/actor/adhoc/PartitionedActor";
            IActor actor = new StandaloneActor(state, actorName,
                new String[] { "CreatePartitionedActorViaActorMethod" },
                new Func<IActorState, Object[], Object>[] { PartitionedActor.CreatePartitionedActorViaActorMethod });

            String json = actor.CallMethod("FakeClient", 1, "", "CreatePartitionedActorViaActorMethod",
                                           new String[0]).Result;
            ActorServiceResult result = ActorServiceResult.FromJson(json);

            if (result.Status != ActorServiceResultStatus.Succeeded)
                throw new Exception(String.Format("Calling an actor method failed!  {0}: {1}\r\n{2}", result.ExceptionType,
                                                  result.ExceptionMessage, result.ExceptionStackTrace));
            if (!result.Result.Contains("succeeded"))
                throw new Exception(String.Format("Actor to Actor Synchronous method call test failed!  Return value: {0}", result));
        }
    }
}
